在上一篇我們建立了 Schema Gratitude.Notes.Note
,現在我們要在 Gratitude.Notes
這個 Context 裡面把資料庫操作包裝起來,之後處理畫面邏輯時可以直接呼叫使用。
我們先盤點一下我們所需要的功能
再預想一下我們之後會想要怎麼使用這些功能
alias Gratitude.Notes
# 讀取全部的 Note
Gratitude.Notes.list_notes()
# 建立 Note
Gratitude.Notes.create_note(%{content: "出門有帶傘"})
# 刪除 Note
Gratitude.Notes.delete_note!(id)
# 更新 Note
Gratitude.Notes.update_note!(id, %{content: "媽媽有提醒我帶傘"})
好了之後就開始吧
在新增檔案 lib/gratitude/notes.ex
,並且定義 Gratitude.Notes
模組
記得 alias Gratitude.Repo
與 Gratitude.Notes.Note
這樣我們可以直接使用 Repo
與 Note
這兩個 module ,讓這邊看起來簡潔一些
defmodule Gratitude.Notes do
alias Gratitude.Repo
alias Gratitude.Notes.Note
end
list_notes/0
這個非常簡單,前一篇我們已經在 iex 裡面使用過了,使用 Repo.all/1
def list_notes do
Repo.all(Note)
end
(雖然我們在這邊直接給他 Note,其實他收的參數是 Queryable,之後有比較複雜的查詢時,會再一起說明。)
create_note/1
如果使用 Repo.insert/1
存進資料庫的話,在內部使用的話沒關係,但是我們主要是要從網頁上傳送使用者填寫的資料,所以我們必須要使用在建立 Schema 時一起建立的 changeset/2 函式。
def create_note(attrs) do
%Note{}
|> Note.changeset(attrs)
|> Repo.insert()
end
changeset (改變集)函式顧名思義就是紀錄了這次改變了什麼,但因為我們這次是新增,所以我們給他一個空的 Note struct ,再給他這次的輸入 attrs,就會回傳一個 changeset,再把這個 changeset 丟給 Repo.insert/1 就可以了,我們來在 iex -S mix
試試看。
alias Gratitude.Notes
alias Gratitude.Notes.Note
Notes.create_note(%{content: "沒帶到錢包,還好口袋有一百塊可以搭車")
#=>
{:ok, %Gr...省略}
還記得我們的 note 有檢查必需要提供 content 欄位的內容嗎?試試看如果我們沒有給的情況:
Notes.create_note(%{})
#=>
{:error,
#Ecto.Changeset<
action: :insert,
changes: %{},
errors: [content: {"can't be blank", [validation: :required]}],
data: #Gratitude.Notes.Note<>,
valid?: false
>}
這個是經典的錯誤 tuple 第一個是 :error,第二個是我們的 changeset ,我們可以看到他的 errors 紀錄了 content 的錯誤,說明 content 欄位不能為空。
所以我們剛剛建立的 create_note/1
,在之後的畫面邏輯中,就可以搭配 case 來決定要回覆使用者建立成功或是失敗的訊息。
update_note!/2
再剛剛新增的時候,我們使用空的 Note struct 與新資料給 Note.changset/2 來建立 changeset,可以想像更新的時候,我們可以使用舊的資料,再跟新的改變一起給 Note.changset/2 來產生改變用的 changeset。
# 我們可以使用 id 拿到現有的 note 資料
note = Repo.get(Note, 1)
# 再把新的資料給 changeset
Note.changeset(note, %{content: "媽媽有提醒我帶傘"})
|> Repo.update()
# 最後呼叫 Repo.update/1 就可以更新資料了
依照上面的作法,我們可以實作 update_note/2
函式,
不過拿現有的資料跟更新他應該是兩件事,我們通常會把 get_note/1
獨立出來。
def get_note(id) do
Repo.get(Note, id)
end
def update_note(note, attrs) do
note
|> Note.changeset(attrs)
|> Repo.update()
end
小技巧:如果我們的 iex -S mix
還沒有關掉的話,會發現新寫的函式還沒有 compile 所以不在裡面,但是如果我們把 iex 關掉,新開一個的話又要重新打剛剛的 alias,這個時候可以直接執行 recompile
來重新 compile 所有的檔案,就可以不用重開了。
來試試看剛剛寫的 update_note/2
吧
note = Notes.get_note(1)
Notes.update_note(note, %{content: "媽媽有提醒我帶傘還有穿雨鞋"})
delete_note!/1
刪除的話就比較簡單了,直接給 Repo.delete/1
就可以了。不過使用時跟剛剛一樣,我們要先找到要刪除的資料。
def delete_note(note) do
Repo.delete(note)
end
這邊可能會想到,這個函式這麼小,是不是可以省略,到時候要使用的時候直接呼叫 Repo.delete(note)
就可以了?
在自己試玩的專案或許可以,但是如果考慮到維護性的話,我們還是建議把把資料處理的邏輯與畫面分開。這些操作都包在一個 module 裡面,這樣之後如果要修改的話,例如要刪除 note 的時候會寄信通知作者,這樣我們只要修改這個 module 就可以了。
來試試看剛剛寫的 delete_note/1
吧
note = Notes.get_note(1)
Notes.delete_note(note)
# 這個時候我們再試著找找看 id 為 1 的 note,會發現已經找不到了
# 回傳的是 nil
Notes.get_note(1)
#=> nil